using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Configuration;
using System.Text;
using PSE.Hosting.ServiceHoster.Configuration;
using System.IO;
using System.ServiceProcess;
using PSE.Hosting.ServiceHoster.Logging;

namespace PSE.Hosting.ServiceHoster.Business
{
    /// <summary>
    /// This class implements a Singleton pattern to manage the services.
    /// </summary>
    public sealed class Services
    {
        #region Fields

        private const string WindowsServiceName = "PSEServiceHoster";
        private static volatile Services _instance;
        private static object _syncRoot = new Object();
        private Dictionary<string, ServiceHosterProcess> _processes = new Dictionary<string, ServiceHosterProcess>();

        #endregion

        #region Constructors

        /// <summary>
        /// Declared as private to prevent class instantiation.
        /// </summary>
        private Services()
        {
            this.StartPackages();
        }

        /// <summary>
        /// Restart all the services contained in the assemblies of a single package.
        /// </summary>
        /// <param name="packageName">The name of the package to be refreshed.</param>
        /// <returns>A list of failure or success messages.</returns>
        public List<MessageData> RefreshPackageAssemblies(string packageName)
        {
            // Stop all assemblies from the package that had a assembly changed.
            this.StopAll(packageName);
            // Start all assemblies from the package that had a assembly changed.
            List<MessageData> messages = this.StartAll(packageName);

            return messages;
        }

        private void StartPackages()
        {
            // Get all the packages from this server.
            lock (_processes)
            {
                List<PackageElement> packages;

                try
                {
                    packages = ConfigurationFile.Instance.GetPackages();
                }
                catch (DirectoryNotFoundException)
                {
                    throw;
                }
                catch (ConfigurationErrorsException ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }

                foreach (PackageElement package in packages)
                {
                    _processes.Add(package.Name, new ServiceHosterProcess(package.Name, package.Domain, package.UserName, package.Password));
                }
            }
        }

        #endregion

        #region Public Members

        /// <summary>
        /// Returns a instance of the <see cref="PSE.Hosting.ServiceHoster.Business.Services"/>.
        /// </summary>
        public static Services Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (_syncRoot)
                    {
                        if (_instance == null)
                        {
                            _instance = new Services();
                        }
                    }
                }
                return _instance;
            }
        }

        /// <summary>
        /// Starts all assemblies of all packages for this machine.
        /// </summary>
        public List<MessageData> StartAll()
        {
            List<MessageData> messagesList = new List<MessageData>();

            List<PackageElement> packages = ConfigurationFile.Instance.GetPackages();

            //_errors = new StringBuilder();
            lock (_processes)
            {
                foreach (string package in _processes.Keys)
                {
                    if (packages.Find(p => p.Name == package).IsStarted)
                    {
                        try
                        {
                            List<MessageData> messages = this.StartAll(package);
                            messagesList.AddRange(messages);
                        }
                        catch (Exception ex)
                        {
                            messagesList.Add(new MessageData(MessageData.MessageTypes.Error, ex.ToString()));
                        }
                    }
                }
            }

            return messagesList;
        }

        /// <summary>
        /// Start all services a specified package.
        /// </summary>
        /// <param name="packageName">The package name to start the services.</param>
        public List<MessageData> StartAll(string packageName)
        {
            List<MessageData> messagesList = new List<MessageData>();

            lock (_processes)
            {
                this.VerifyPackageExists(packageName);

                List<AssemblyElement> assemblies = ConfigurationFile.Instance.GetAssemblies(packageName);

                messagesList = this.GetPackage(packageName, true).StartAll(true);

                ConfigurationFile.Instance.SavePackageStarted(packageName);
            }

            return messagesList;
        }

        /// <summary>
        /// Stop Package
        /// </summary>
        /// <param name="packageName">Package Name</param>
        public void StopPackage(string packageName)
        {
            StopAll(packageName, false);
        }

        /// <summary>
        /// Start Package
        /// </summary>
        /// <param name="packageName">Package Name</param>
        public void StartPackage(string packageName)
        {
            lock (_processes)
            {
                this.VerifyPackageExists(packageName);

                if (!_processes[packageName].IsStarted)
                {
                    _processes[packageName].StartProcess();

                    _processes[packageName].StartAll(false);

                    ConfigurationFile.Instance.SavePackageStarted(packageName);
                }
            }
        }

        /// <summary>
        /// Stop all the services from an assembly.
        /// </summary>
        /// <param name="packageName">The name of the package containing the assembly to be stopped.</param>
        /// <param name="codeBase">The name of the assembly to be stopped.</param>
        public void StopAssembly(string packageName, string codeBase)
        {
            this.GetPackage(packageName, false).StopAssembly(codeBase);
        }

        /// <summary>
        /// Start all WCF services from a specific assembly.
        /// </summary>
        /// <param name="packageName">The name of the package of the assembly to start.</param>
        /// <param name="fileName">The name of the assembly file to start. This is the filename only, without the path.</param>
        public List<MessageData> StartAssembly(string packageName, string fileName)
        {
            return this.GetPackage(packageName, false).StartAssembly(fileName);
        }

        /// <summary>
        /// Starts a Service configured within the Hoster app.config.
        /// </summary>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to start.</param>
        public void StartService(string packageName, string assembly, string serviceName)
        {
            this.GetPackage(packageName, false).StartService(assembly, serviceName);
        }

        /// <summary>
        /// Stops the specified service.
        /// </summary>
        /// <param name="packageName">The name of the package containing the service.</param>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to stop.</param>
        public void StopService(string packageName, string assembly, string serviceName)
        {
            this.GetPackage(packageName, false).StopService(assembly, serviceName);
        }

        /// <summary>
        /// Stops all the services already started.
        /// </summary>
        public void StopAll()
        {
            lock (_processes)
            {
                foreach (string package in _processes.Keys)
                {
                    this.StopAll(package, false);
                }

                _processes.Clear();
            }

            this.StartPackages();
        }

        /// <summary>
        /// Stops all the services already started from a specific package.
        /// </summary>
        public void StopAll(string packageName)
        {
            this.StopAll(packageName, true);
        }

        private void StopAll(string packageName, bool save)
        {
            lock (_processes)
            {
                this.VerifyPackageExists(packageName);

                if (_processes[packageName].IsStarted)
                {
                    _processes[packageName].StopAll();
                    _processes[packageName].CloseProcess();
                }

                if (save)
                    ConfigurationFile.Instance.SavePackageStoped(packageName);
            }
        }

        /// <summary>
        /// Gets a list of all the services configured within the Service Hoster app.config.
        /// </summary>
        /// <returns></returns>
        public List<Entities.Service> GetServiceList(string packageName, string assembly)
        {
            return this.GetPackage(packageName, false).GetServiceList(assembly);
        }

        /// <summary>
        /// Gets a list of all packages created on this server.
        /// </summary>
        /// <returns>List of packages.</returns>
        public List<Entities.Package> GetPackageList()
        {
            lock (_processes)
            {
                List<Entities.Package> list = new List<Entities.Package>();

                foreach (ServiceHosterProcess process in _processes.Values)
                {
                    Entities.Package package = new Entities.Package(process.Id, process.PackageName, process.Domain, process.UserName);
                    list.Add(package);
                }

                return list;
            }
        }

        /// <summary>
        /// Returns a list of all the assemblies added in a package.
        /// </summary>
        /// <param name="packageName">The name of the package to get the list of assenblies.</param>
        /// <returns>List of added assemblies.</returns>
        public List<Entities.Assembly> GetAssemblyList(string packageName)
        {
            return this.GetPackage(packageName, false).GetAssemblyList();
        }

        /// <summary>
        /// Adds a new package to the Service Hoster.
        /// </summary>
        /// <param name="packageName">The name of the package to be added.</param>
        public void AddPackage(string packageName, string domain, string userName, string password)
        {
            packageName = packageName.ToUpper();

            lock (_processes)
            {
                if (_processes.ContainsKey(packageName))
                {
                    throw new Business.PackageAlreadyExistsException(string.Format("The '{0}' package already exists", packageName));
                }

                this.ValidateLogin(ref domain, ref userName, ref password);

                // Adds the new package to the configuration file.
                try
                {
                    ConfigurationFile.Instance.AddPackage(packageName, domain, userName, password);
                }
                catch (Configuration.PackageAlreadyExistsException)
                {
                    throw new Business.PackageAlreadyExistsException(string.Format("The '{0}' package already exists", packageName));
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw new Exception("Problems adding the package.", ex);
                }

                // Add the package to the package collection;
                _processes.Add(packageName, new ServiceHosterProcess(packageName, domain, userName, password));
            }

            Logger.Log(string.Format("Package {0} was added.", packageName), Logger.EntryType.Information, Logger.Source.ServiceHoster);
        }

        /// <summary>
        /// Update a package in the Service Hoster.
        /// </summary>
        /// <param name="packageName">The name of the package to be added.</param>
        public void UpdatePackage(string packageName, string domain, string userName, string password)
        {
            packageName = packageName.ToUpper();

            lock (_processes)
            {
                this.VerifyPackageExists(packageName);
                this.ValidateLogin(ref domain, ref userName, ref password);

                bool start = false;

                // Stops all assemblies from this package
                if (_processes[packageName].IsStarted)
                {
                    this.StopAll(packageName);
                    start = true;
                }

                // Update the package from the configuration.
                ConfigurationFile.Instance.UpdatePackage(packageName, domain, userName, password);

                _processes[packageName].Domain = domain;
                _processes[packageName].UserName = userName;
                _processes[packageName].Password = password;

                // Start all assemblies from this package
                if (start)
                    this.StartAll(packageName);
            }

            Logger.Log(string.Format("Package {0} was updated.", packageName), Logger.EntryType.Information, Logger.Source.ServiceHoster);
        }

        /// <summary>
        /// Removes a package from the Service Hoster.
        /// </summary>
        /// <param name="packageName"></param>
        public void RemovePackage(string packageName)
        {
            packageName = packageName.ToUpper();

            lock (_processes)
            {
                this.VerifyPackageExists(packageName);

                // Stops all assemblies from this package
                if (_processes[packageName].IsStarted)
                    this.StopAll(packageName);

                // Removes the package from the configuration.
                ConfigurationFile.Instance.RemovePackage(packageName);
                _processes.Remove(packageName);
            }

            Logger.Log(string.Format("Package {0} was removed.", packageName), Logger.EntryType.Information, Logger.Source.ServiceHoster);
        }

        /// <summary>
        /// Adds a Service to the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="codeBase">The assembly code base.</param>
        /// <param name="domain">The domain name of the user.</param>
        /// <param name="userName">The user name for impersonate service operations.</param>
        /// <param name="password">The user password.</param>
        public void AddAssembly(string packageName, string codeBase/*, string domain, string userName, string password*/)
        {
            this.GetPackage(packageName, false).AddAssembly(codeBase/*, domain, userName, password*/);
        }

        /// <summary>
        /// Removes a Service from the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="assemblyFile">The name of the assembly file (without path).</param>
        public void RemoveAssembly(string packageName, string assemblyFile)
        {
            this.GetPackage(packageName, false).RemoveAssembly(assemblyFile);
        }

        #endregion

        private void VerifyPackageExists(string packageName)
        {
            if (!_processes.ContainsKey(packageName))
                throw new InvalidPackageException(string.Format("The '{0}' package does not exists", packageName));
        }

        private void ValidateLogin(ref string domain, ref string userName, ref string password)
        {
            if (string.IsNullOrEmpty(domain) && !string.IsNullOrEmpty(userName))
                domain = System.Environment.MachineName;

            if (!string.IsNullOrEmpty(domain) && !string.IsNullOrEmpty(userName))
            {
                domain = domain.ToUpper();

                Impersonation imp = new Impersonation(domain, userName, password);
                imp.ValidateLogin();
            }
            else
            {
                domain = null;
                userName = null;
                password = null;
            }
        }

        private ServiceHosterProcess GetPackage(string packageName, bool start)
        {
            lock (_processes)
            {
                this.VerifyPackageExists(packageName);

                if (!_processes[packageName].IsStarted && start)
                    _processes[packageName].StartProcess();

                return _processes[packageName];
            }
        }

        #region FileSystemWatcher and Hot Deploy Methods

        /// <summary>
        /// Stop the WCF Service Hoster windows service.
        /// </summary>
        public void StopWindowsService()
        {
            ServiceController controller = new ServiceController(WindowsServiceName);
            controller.Stop();
        }

        /// <summary>
        /// Start the WCF Service Hoster windows service.
        /// </summary>
        public void StartWindowsService(bool asynchronous, bool startAll)
        {
            ServiceController controller = new ServiceController(WindowsServiceName);
            controller.Start(new string[] { asynchronous.ToString(), startAll.ToString() });
        }

        #endregion
    }
}
